﻿// -----------------------------------------------------------------------
// <copyright file="Request.cs" company="Softserve">
// Опрацьовую вхідну інформацію і парсить її
// </copyright>
// -----------------------------------------------------------------------
namespace WebServer.BusinessLogic
{
    using System;
    using System.Text;
    using System.Text.RegularExpressions;
    using WebServer.BusinessLogic.Logs;
    using WebServer.BusinessLogic.Web;
    
    /// <summary>
    /// Enum for Http Methods
    /// </summary>
    public enum EnumHttpMethod
    {
        /// <summary>
        /// for GET Metod
        /// </summary>
        GET,

        /// <summary>
        /// for POST Metod
        /// </summary>
        POST,

        /// <summary>
        /// for something else
        /// </summary>
        UNKNOWN,
    }

    /// <summary>
    /// Enum for compressing data
    /// </summary>
    public enum EnumAcceptEncoding
    {
        /// <summary>
        /// Doesn't accept compression
        /// </summary>
        None,

        /// <summary>
        /// Accepts Gzip compression
        /// </summary>
        Gzip
    }

    /// <summary>
    /// Create instance of request class
    /// </summary>
    public class Request
    {
        /// <summary>
        /// Regex for finding Accept Encoding
        /// </summary>
        private static Regex regexAcceptEncoding = new Regex(@"Accept\-Encoding:\s*(.*?)\s*$", RegexOptions.Compiled | RegexOptions.Multiline);

        /// <summary>
        /// Regex for finding host
        /// </summary>
        private static Regex regexHost = new Regex(@"Host:\s*(.*?)\s*$", RegexOptions.Compiled | RegexOptions.Multiline);

        /// <summary>
        /// Regex for finding UserAgent
        /// </summary>
        private static Regex regexUserAgent = new Regex(@"User\-Agent:\s*(.*?)\s*$", RegexOptions.Compiled | RegexOptions.Multiline);

        /// <summary>
        /// Keep metod of HTTP request
        /// </summary>
        private EnumHttpMethod httpMethod;
        
        /// <summary>
        /// Keep version of HTTP request
        /// </summary>
        private string httpVersion;
        
        /// <summary>
        /// Keep path from HTTP request
        /// </summary>
        private string httpPath;
        
        /// <summary>
        /// Keep all request text
        /// </summary>
        private string rawData;
        
        /// <summary>
        /// Keep information about Range
        /// </summary>
        private long rangeStart;
        
        /// <summary>
        /// Keep host 
        /// </summary>
        private string host;
        
        /// <summary>
        /// Keep User-Agent
        /// </summary>
        private string userAgent;
        
        /// <summary>
        /// Keep Accept-Encoding
        /// </summary>
        private EnumAcceptEncoding acceptEncoding = EnumAcceptEncoding.None;
        
        /// <summary>
        /// Keep information about cookies
        /// </summary>
        private string cookiesRaw;
        
        /// <summary>
        /// Keep information about having cookies in request
        /// </summary>
        private bool hasCookie;
       
        /// <summary>
        /// Keep information about having Range
        /// </summary>
        private bool hasRange;
         
        /// <summary>
        /// Initializes a new instance of the <see cref="Request" /> class.
        /// </summary>
        public Request()
        {
            this.httpMethod = EnumHttpMethod.UNKNOWN;
            this.httpVersion = Helpers.WebserverConstants.Unknown;
            this.hasRange = false;
            this.hasCookie = false;           
        }

        /// <summary>
        /// Gets or sets keep information about cookies
        /// </summary>
        public WebsiteCookies Cookies { get; set; }

        /// <summary>
        /// Gets or sets information about http method
        /// <Gets>Gets HttpMethod</Gets>
        /// <sets>Sets HttpMethod</sets>
        /// </summary>
        public EnumHttpMethod HttpMethod
        {
            get { return this.httpMethod; }
            set { value = this.httpMethod; }
        }

        /// <summary>
        /// Gets property for keeping version of HTTP request
        /// <Gets>Gets httpVersion</Gets>
        /// <sets>Sets httpVersion</sets>
        /// </summary>
        public string HttpVersion
        {
            get { return this.httpVersion; }
        }

        /// <summary>
        /// Gets RawData
        /// <Gets>RawData</Gets>
        /// </summary>
        public string RawData
        {
            get { return this.rawData; }
        }

        /// <summary>
        /// Gets information about Range
        /// <Gets>a number of RangeStart</Gets>
        /// </summary>
        public long RangeStart
        {
            get { return this.rangeStart; }
        }

        /// <summary>
        /// Gets or sets Host 
        /// <Gets>host</Gets>
        /// </summary>
        public string Host
        {
            get { return this.host; }
            set { this.host = value; }
        }

        /// <summary>
        /// Gets or sets User-Agent
        /// <Gets>User agent</Gets>
        /// </summary>
        public string UserAgent
        {
            get { return this.userAgent; }
            set { this.userAgent = value; }
        }

        /// <summary>
        /// Gets or sets Accept-Encoding
        /// <Gets>EnumAcceptEncoding for compressions like None or Gzip</Gets>
        /// </summary>
        public EnumAcceptEncoding AcceptEncoding
        {
            get { return this.acceptEncoding; }
            set { this.acceptEncoding = value; }
        }

        /// <summary>
        /// Gets or sets the http path.
        /// </summary>
        public string HttpPath
        {
            get { return this.httpPath; }
            set { this.httpPath = value; }
        }

        /// <summary>
        /// Gets or sets Cookies String
        /// <Gets>string with information about cookies</Gets>
        /// </summary>
        public string CookiesRaw
        {
            get { return this.cookiesRaw; }
            set { this.cookiesRaw = value; }
        }

        /// <summary>
        /// Gets a value indicating whether is field Range in http request
        /// <gets>is field Range in request</gets>
        /// </summary>
        public bool HasRange
        {
            get { return this.hasRange; }
        }

        /// <summary>
        /// Gets a value indicating whether is Cookies
        /// <gets>is field Cookie in request</gets>
        /// </summary>
        public bool HasCookie
        {
            get { return this.hasCookie; }
        }

        /// <summary>
        /// Gets a value indicating whether request is correct
        /// </summary>
        /// <author>VK</author>
        public bool IsCorrect { get; private set; }

        /// <summary>
        /// Gets or sets SessionKey variable for user verification. Can be null when not logged in
        /// </summary>
        public string SessionKey { get; set; }

        /// <summary>
        /// Gets or sets IP Address of remote user, for Session creation
        /// </summary>
        public string UserIpAddress { get; set; }

        /// <summary>
        /// Main Metod for parsing request
        /// </summary>
        /// <param name="data">Array of HTTP request</param>
        public void ParseRequest(byte[] data)
        {
            //// VK: set correct request
            this.IsCorrect = true;

            // Make all request ToString()
            string requestLine = Encoding.ASCII.GetString(data);

            // VK: fix \0\0\0\0\0\0\0 request line by checking for existance of "\r\n":
            int carriageReturn = requestLine.IndexOf("\r\n", StringComparison.CurrentCulture);
            if (carriageReturn < 0)
            {
                // log error in request
                Logger.Instance.Log("No CR+LF in request, so header is not present. Also request is not in correct format.");      
         
                // set incorrect request
                this.IsCorrect = false;

                // stop processing it by returning the method
                return;
            }

            // Store header line
            string headLine = requestLine.Substring(0, carriageReturn);

            // VK: use Trace instead of Console, prevents from writing to the response stream?
            // Trace.Write(requestLine);
            // Parsing information
            this.ParseStartingLine(headLine);
            this.ParseCookies(requestLine);
            this.ParseHost(requestLine);
            this.ParseUserAgent(requestLine);
            this.ParseAcceptEncoding(requestLine);
            this.FillRawData(requestLine);
            this.FillRange(requestLine);
        }

        /// <summary>
        /// Accept HTTP header line.
        /// Processing and filling 
        /// </summary>
        /// <param name="startingLine">text of HTTP header line</param>
        private void ParseStartingLine(string startingLine)
        {
            // Parsing HTTPversion
            if (startingLine.StartsWith("GET"))
            {
                this.httpMethod = EnumHttpMethod.GET;
            }
            else
            {
                if (startingLine.StartsWith("POST"))
                {
                    this.httpMethod = EnumHttpMethod.POST;
                }
            }

            // Console.WriteLine(HttpMethod);
            // Parsing HTML version
            this.httpVersion = Helpers.WebserverConstants.Unknown;
            if (startingLine.Contains(Helpers.WebserverConstants.HttpVersion11))
            {
                this.httpVersion = Helpers.WebserverConstants.HttpVersion11;
            }
            else if (startingLine.Contains(Helpers.WebserverConstants.HttpVersion10))
            {
                this.httpVersion = Helpers.WebserverConstants.HttpVersion10;
            }

            // Console.WriteLine(httpVersion);
            // Parsing path
            string pathLine = startingLine.Substring(startingLine.IndexOf("/", StringComparison.CurrentCulture));
            this.httpPath = pathLine.Substring(0, pathLine.IndexOf(" ", StringComparison.CurrentCulture));
        }

        /// <summary>
        /// Parsing Host in request
        /// </summary>
        /// <param name="message">request string</param>
        private void ParseHost(string message)
        {
            // LasIndexOf was using 46% of all the code while 100 concurent users were getting an image and main page.
            // Therefore changing to Regex.Compiled made difference
            var match = regexHost.Match(message);
            if (match.Success)
            {
                this.host = match.Groups[1].Value;
            }      
        }

        /// <summary>
        /// Parsing Host in request
        /// </summary>
        /// <param name="message">request string</param>
        private void ParseUserAgent(string message)
        {
            // Regex.Compiled is 4 times faster than IndexOf
            var match = regexUserAgent.Match(message);
            if (match.Success)
            {
                this.userAgent = match.Groups[1].Value;
            }
        }

        /// <summary>
        /// Parsing Accept-Encoding in request
        /// </summary>
        /// <param name="message">request string</param>
        private void ParseAcceptEncoding(string message)
        {
            var match = regexAcceptEncoding.Match(message);
            if (match.Success)
            {
                if (match.Groups[1].Value.Contains("gzip"))
                {
                    this.acceptEncoding = EnumAcceptEncoding.Gzip;
                }
                else
                {
                    this.acceptEncoding = EnumAcceptEncoding.None;
                }
            }
        }

        /// <summary>
        /// Find cookies in request and process
        /// </summary>
        /// <param name="message">request string</param>
        private void ParseCookies(string message)
        {
            string wordToParse = "Cookie";
            if (message.Contains(wordToParse))
            {
                this.hasCookie = true;
                string hostString = message.Substring(message.LastIndexOf(wordToParse, StringComparison.CurrentCulture));
                hostString = hostString.Substring(wordToParse.Length + 1, hostString.IndexOf("\r\n", StringComparison.CurrentCulture));
                this.cookiesRaw = hostString.Contains("\r\n") ? hostString.Remove(hostString.IndexOf("\r\n", StringComparison.CurrentCulture)).Trim() : hostString.Trim();

                // parse into cookies
                if (!Configurator.Instance.IsCookielessSession)
                {
                    this.Cookies = new WebsiteCookies();
                    var cookies = this.cookiesRaw.Split(new[] { ';' });
                    foreach (var cooky in cookies)
                    {
                        var cookyKeyValue = cooky.Split(new[] { '=' });
                        if (cookyKeyValue.Length == 2)
                        {
                            string key = cookyKeyValue[0].Trim();
                            string value = cookyKeyValue[1].Trim();
                            this.Cookies.Add(key, value);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Filling all request in RawData
        /// </summary>
        /// <param name="message">request string</param>
        private void FillRawData(string message)
        {
            this.rawData = message;
        }

        /// <summary>
        /// Trying to find field "Range" in request
        /// If found - fill 
        /// </summary>
        /// <param name="message">request string</param>
        private void FillRange(string message)
        {
            if (message.Contains("Range"))
            {
                this.hasRange = true;
                string rangeString = message.Substring(message.IndexOf("Range", StringComparison.CurrentCulture));
                rangeString = rangeString.Substring(0, rangeString.IndexOf("\r\n", StringComparison.CurrentCulture));

                // Make line like "Range: bytes=88080384-
                rangeString = rangeString.Substring(rangeString.IndexOf("=", StringComparison.CurrentCulture) + 1);
                rangeString = rangeString.Substring(0, rangeString.IndexOf("-", StringComparison.CurrentCulture));
                try
                {
                    this.rangeStart = Convert.ToInt64(rangeString);
                }
                catch (FormatException)
                {
                    this.rangeStart = 0;
                    this.hasRange = false;
                }
            }
        }
    }
}
